/********************************************************/
/* Aluno: Luciano G. S. Ramalho                         */
/* Numero USP: 1002101                                  */
/* Exercicio-Programa 4 -- Transformações sobre Imagens */
/* MAC110 (BCC) -- 2007 -- Professor: Reverbel          */
/* Compilador: gcc (GCC) 4.0.3 (Ubuntu 4.0.3-1ubuntu5)  */
/********************************************************/

#include <stdio.h>
#include <stdlib.h> /* para chamar a função system */
#include <string.h> /* para chamar a função strcmp */
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <ctype.h>

int killpg(pid_t pgrp, int signal);
int setpgrp(void);

#define FALSE        0
#define TRUE         1
#define MAX_NAME     256   /* tamanho maximo para nome de arquivo */
#define MAX_DIM      512   /* dimensao maxima para matrizes */
#define MAX_MAXVAL   255   /* valor máximo de maxval, a cor branca */   
#define MAX_LINE     256   /* comprimento maximo de linha da entrada */
#define TMPFILE_NAME "temp.pgm"
#define VIEWER       "eog"
#define PGM_BIN      1     /* indica formato PGM binário */
#define PGM_ASCII    2     /* indica formato PGM ASCII */

/* leitura e gravaçõa de arquivos */
int read_image_file(int *m, int *n, int *maxval, int a[][MAX_DIM]);
int skip_whitespace_and_comments(FILE *in);
int write_ascii_file(int m, int n, int maxval, int a[][MAX_DIM], char name[]);
int write_bin_file(int m, int n, int maxval, int a[][MAX_DIM], char name[]);
/* transformações geométricas sobre a imagem */
void horizontal_flip(int m, int n, int a[][MAX_DIM]);
void vertical_flip(int m, int n, int a[][MAX_DIM]);
void rotate90cw(int *m, int *n, int a[][MAX_DIM]);
void rotate90ccw(int *m, int *n, int a[][MAX_DIM]);
void rotate180(int m, int n, int a[][MAX_DIM]);
/* funcões auxiliares das transformações geómetricas */
void transpose(int *m, int *n, int a[][MAX_DIM]);
void swap(int *i, int *j);
/* filtro da mediana e funções auxiliares */
void median_filter(int m, int n, int a[][MAX_DIM]);
int median(int a[][MAX_DIM], int pix_i, int pix_j, int dv, int dh);
void insertion_sort(int v[], int len);
void copy(int m, int n, int a[][MAX_DIM], int b[][MAX_DIM]);
/* funções auxiliares de interação com o usuário */
int run_viewer_and_get_option(char command[], char message[]);
int get_option(char message[]);

int main() 
{
    char menu[] = 
     "\n"
     "Opcões:\n"
     "  0 - ler nova imagem                   5 - rotação de 180 graus\n"
     "  1 - inversão horizontal               6 - filtro da mediana\n"
     "  2 - inversão vertical                 7 - salvar em arquivo PGM normal\n"
     "  3 - rotação horária de 90 graus       8 - salvar em arquivo PGM ASCII\n"
     "  4 - rotação anti-horária de 90 graus  9 - sair\n"
     "\n"
     "Escolha uma opção: ";

    /* Comando para visualização de imagem. Este comando usa o visualizador 
       eog ("Eye of Gnome"), para Linux. Outra alternativa é o kview, também
       para Linux. */  
    char image_viewer_cmd[] = VIEWER " " TMPFILE_NAME; 
    char line[MAX_LINE];  /* buffer para entrada de linhas */
    char fname[MAX_NAME]; /* nome do arquivo */

    int m, n;   /* linhas e colunas da imagem */
    int maxval; /* valor máximo da imagem (representa a cor branca) */
    int a[MAX_DIM][MAX_DIM]; /* matriz de pontos da imagem (raster) */
    int option; /* opção selecionada no menu */

    while (!(read_image_file(&m, &n, &maxval, a) 
             && write_ascii_file(m, n, maxval, a, TMPFILE_NAME))) 
        ;
    option = run_viewer_and_get_option(image_viewer_cmd, menu);
    do {
        switch (option) {
            
        case 0: /* ler nova imagem */
            if (read_image_file(&m, &n, &maxval, a) 
                && write_ascii_file(m, n, maxval, a, TMPFILE_NAME))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;
        case 1: /* inversão horizontal */
            horizontal_flip(m, n, a);
            if (write_ascii_file(m, n, maxval, a, TMPFILE_NAME))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;
        case 2: /* inversão vertical */
            vertical_flip(m, n, a);
            if (write_ascii_file(m, n, maxval, a, TMPFILE_NAME))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;
        case 3: /* rotação horária de 90 graus */
            rotate90cw(&m, &n, a);
            if (write_ascii_file(m, n, maxval, a, TMPFILE_NAME))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;
        case 4: /* rotação anti-horária de 90 graus */
            rotate90ccw(&m, &n, a);
            if (write_ascii_file(m, n, maxval, a, TMPFILE_NAME))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;
        case 5: /* rotação de 180 graus */
            rotate180(m, n, a);
            if (write_ascii_file(m, n, maxval, a, TMPFILE_NAME))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;
        case 6: /* filtro da mediana */
            median_filter(m, n, a);
            if (write_ascii_file(m, n, maxval, a, TMPFILE_NAME))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;
        case 7: /* salvar em arquivo PGM normal */
            do {
                printf("\nDigite o nome do arquivo de saída: ");
                fgets(line, MAX_LINE, stdin);
            } while (sscanf(line, "%s", fname) != 1);
            if (write_bin_file(m, n, maxval, a, fname))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;                  
        case 8: /* salvar em arquivo PGM ASCII */
            do {
                printf("\nDigite o nome do arquivo de saída: ");
                fgets(line, MAX_LINE, stdin);
            } while (sscanf(line, "%s", fname) != 1);
            if (write_ascii_file(m, n, maxval, a, fname))
                option = run_viewer_and_get_option(image_viewer_cmd, menu); 
            else
                option = get_option(menu);
            break;                  
        case 9:
            break;
        default:
            printf("Opção inválida\n");
            option = get_option(menu);
            break;
        }
    } while (option != 9);
    remove(TMPFILE_NAME);
    return 0;
}

/* read_image_file: Ler arquivo de imagem em formato PGM ASCII ou binário. */
int read_image_file(int *m, int *n, int *maxval, int a[][MAX_DIM])
{
    char line[MAX_LINE];
    char fname[MAX_NAME];
    FILE *entrada;
    int i, j, aux;
    char key[128];
    int formato;  /* PGM_BIN ou PGM_ASCII */

    do {
        printf("\nDigite o nome do arquivo de entrada: ");
        fgets(line, MAX_LINE, stdin);
    } while (sscanf(line, "%s", fname) != 1);
    
    /* abrir arquivo para leitura */
    entrada = fopen(fname, "r");
    if (entrada == NULL) {
        printf("Erro na abertura do arquivo %s.\n", fname);
        return FALSE;
    }
    /* ler prefixo */
    aux = fscanf(entrada, "%s", key);
    if (aux != 1) {
        printf("Erro na leitura do arquivo %s.\n", fname);
        fclose(entrada);
        return FALSE;
    }
    /* determinar tido do arquivo */
    if (strcmp(key, "P2") == 0)
        formato = PGM_ASCII;
    else if (strcmp(key, "P5") == 0)
        formato = PGM_BIN;
    else {
        printf("Formato desconhecido.\n");
        fclose(entrada);
        return FALSE;
    }
    skip_whitespace_and_comments(entrada);
    /* ler largura (número de colunas) */
    aux = fscanf(entrada, "%d", n);
    if (aux != 1) {
        printf("Formato incorreto: faltou n.\n");
        fclose(entrada);
        return FALSE;
    }
    skip_whitespace_and_comments(entrada);
    /* ler altura (número de linhas) */
    aux = fscanf(entrada, "%d", m);
    if (aux != 1) {
        printf("Formato incorreto: faltou m.\n");
        fclose(entrada);
        return FALSE;
    }
    if ((*m > MAX_DIM) || (*n > MAX_DIM)) {
        printf("A imagem %s excede o limite de %d x %d pixels deste programa.\n",
               fname, MAX_DIM, MAX_DIM);
        fclose(entrada);
        return FALSE;
    }
    skip_whitespace_and_comments(entrada);
    /* ler maxval */
    aux = fscanf(entrada, "%d", maxval);
    if (aux != 1) {
        printf("Formato incorreto: faltou maxval.\n");
        fclose(entrada);
        return FALSE;
    }
    if (*maxval > MAX_MAXVAL) {
        printf("A imagem %s excede a profundidade de cor deste programa (%d).\n",
               fname, MAX_MAXVAL);
        fclose(entrada);
        return FALSE;
    }
    skip_whitespace_and_comments(entrada);
    /* ler a matriz da imagem (raster) */
    if (formato == PGM_ASCII) {
        for (i = 0; i < *m; i++) {
            for (j = 0; j < *n; j++) {
                aux = fscanf(entrada, "%d ", &a[i][j]);
                if (aux != 1) {
                    printf("Arquivo PGM ASCII corrompido.\n");
                    fclose(entrada);
                    return FALSE;
                }
            }
        }
    } else { /* formato == PGM_BIN */
        for (i = 0; i < *m; i++) {
            for (j = 0; j < *n; j++) {
                aux = fgetc(entrada);
                if (aux != EOF)
                    a[i][j] = aux;
                else {
                    printf("Arquivo PGM binário corrompido.\n");
                    fclose(entrada);
                    return FALSE;
                }
            }
        }               
    }
    fclose(entrada);   /* fecha arquivo */
    return TRUE;
}

/* skip_whitespace_and_comments: Ler e descartar espaços, TABs, CRs, LFs e
   qualquer caratere que apareça entre um # e um CR ou LF inclusive. */
int skip_whitespace_and_comments(FILE *in)
{
    int c;
    int in_comment = FALSE;
    do {
        c = fgetc(in);
        if (c == EOF)
            break;
        if (c == '#')
            in_comment = TRUE;   /* comentário começa com # */
        else if (in_comment && ((c == '\n') || (c == '\r')))
            in_comment = FALSE;  /* comentário termina no final da linha */
    } while (in_comment || isspace(c));
    if (! isspace(c)) ungetc(c, in); /* devolver caractere não branco */
    return c;
}

/* write_ascii_file: Escrever arquivo PGM no formato ASCII. */
int write_ascii_file(int m, int n, int maxval, int a[][MAX_DIM], char name[])
{
    FILE *saida;
    int i, j;

    /* abre arquivo para escrita */
    saida = fopen(name, "w");
    if (saida == NULL) {
        printf("Erro na abertura do arquivo %s\n", name);
        return FALSE;
    }
    
    fprintf(saida, "P2\n");
    fprintf(saida, "%d %d\n", n, m);
    fprintf(saida, "%d\n", maxval);
    
    for (i = 0; i < m; i++) {
        for (j = 0; j < n; j++) {
            fprintf(saida, "%d ", a[i][j]);
        }
        fprintf(saida, "\n");
    }
    if (fclose(saida) != 0)
        return FALSE;
    return TRUE;
}

/* write_bin_file: Escrever arquivo PGM no formato binário. */
int write_bin_file(int m, int n, int maxval, int a[][MAX_DIM], char name[])
{
    FILE *saida;
    int i, j;
    int code;

    /* abre arquivo para escrita */
    saida = fopen(name, "wb");
    if (saida == NULL) {
        printf("Erro na abertura do arquivo %s\n", name);
        return FALSE;
    }
    
    fprintf(saida, "P5\n");
    fprintf(saida, "%d %d\n", n, m);
    fprintf(saida, "%d\n", maxval);
    
    for (i = 0; i < m; i++) {
        for (j = 0; j < n; j++) {
            code = fputc(a[i][j], saida);
            if (code == EOF) {
                printf("Erro ao escrever arquivo %s\n", name);
                fclose(saida);
                return FALSE;
            }
        }
    }
    if (fclose(saida) != 0)
        return FALSE;
    return TRUE;
}

/* horizontal_flip: Inverter a matriz a pela troca (swap) das células
   simétricas em relação ao eixo vertical, evitando a alocação de outra matriz.
*/
void horizontal_flip(int m, int n, int a[][MAX_DIM])
{
    int i, j;

    for (i = 0; i < m; i++)
        /* No flip horizontal basta percorrer metade das colunas. */
        /* Se n é impar, a coluna central (n/2) não precisa ser alterada. */ 
        for (j = 0; j < (n/2); j++) 
            swap(&a[i][j],&a[i][n-j- 1]);
}

/* vertical_flip: Inverter a matriz a pela troca (swap) das células simétricas
   em relação ao eixo horizontal, evitando a alocação de outra matriz.
*/
void vertical_flip(int m, int n, int a[][MAX_DIM])
{
    int i, j;

    /* No flip vertical basta percorrer metade das linhas. */
    /* Se m é impar, a linha central (m/2) não precisa ser alterada. */ 
    for (i = 0; i < (m/2); i++) 
        for (j = 0; j < n; j++) 
            swap(&a[i][j],&a[m-i-1][j]);
}

/* rotate90cw: Rotacionar 90 graus no sentido horário, aproveitando a
   funcionalidade de transpose e horizontal_flip; m e n são passados
   como ponteiros porque seus valores são trocados em transpose.
*/
void rotate90cw(int *m, int *n, int a[][MAX_DIM])
{
    transpose(m, n, a);
    horizontal_flip(*m, *n, a);
}

/* rotate90ccw: Rotacionar 90 graus no sentido anti-horário, aproveitando
   a funcionalidade de transpose e vertical_flip; m e n são passados
   como ponteiros porque seus valores são trocados em transpose.
*/
void rotate90ccw(int *m, int *n, int a[][MAX_DIM])
{
    transpose(m, n, a);
    vertical_flip(*m, *n, a);
}

/* rotate180: Rotacionar 180 graus, aproveitando a funcionalidade de 
   horizontal_flip e vertical_flip. */
void rotate180(int m, int n, int a[][MAX_DIM])
{
    horizontal_flip(m, n, a);
    vertical_flip(m, n, a);
}

/* transpose: Transpor as linhas da matriz a para colunas em a mesmo, e trocar
   (swap) as dimensões m e n. Com esta função, as operações de rotação em
   90 graus podem ser implementadas de maneira simples com um transpose e um
   flip apropriado, como se nota nas funções rotate90cw e rotate90ccw.
*/
void transpose(int *m, int *n, int a[][MAX_DIM])
{
    int i, j;
    int min, max; /* dimensões: menor e maior */
    
    min = *m < *n ? *m : *n;
    max = *m > *n ? *m : *n;

    /* Este algoritmo não faz distinção entre fileiras horizontais (linhas)
       ou fileiras verticais (colunas). Ele simplesmente percorre as fileiras
       da menor dimensão e, dentro de cada fileira, troca cada célula situada
       depois da diagonal central da matriz pela sua simétrica em relação à
       diagonal central.
    */
    for (i = 0; i < min; i++) {
        for (j = (i+1); j < max; j++) {
            swap(&a[i][j],&a[j][i]);
        }
    }

    /* Trocar número de linhas pelo número de colunas. */
    swap(m,n);
}

/* swap: Trocar os valores de i e j; usado nas funções de flip e transpose. */
void swap(int *i, int *j) {
    int temp;
    temp = *i;
    *i = *j;
    *j = temp;
}

/* median_filter: Aplicar o filtro da mediana na matriz a com m linhas e n
   colunas; o resultado é calculado na matriz b e depois copiado para a. */
void median_filter(int m, int n, int a[][MAX_DIM])
{
    int i, j;
    int b[MAX_DIM][MAX_DIM];
    int dv, dh;  /* semivariacao vertical e horizontal) */
    
    dh = get_option("Digite a semi-variação horizontal: ");
    dv = get_option("Digite a semi-variação vertical  : ");
    
    /* Processar a imagem somente se ao menos uma semi-variação > 0. */
    if ((dv>=1) || (dh>=1)) {
        for (i = 0; i < m; i++) {
            for (j = 0; j < n; j++) {
                /* Se a vizinhaça exceder os limites da imagem, usar o valor 0
                   conforme instrução na página 2 do enunciado do EP4. */
                if ((i < dv) || ((i+dv) > (m-1)) ||
                    (j < dh) || ((j+dh) > (n-1)))
                    b[i][j] = 0; 
                else /* Dentro dos limites: calcular mediana. */
                    b[i][j] = median(a, i, j, dv, dh);
            }
        }
        copy(m, n, b, a);
    }
}

/* median: Retornar a mediana de uma vizinhança na matriz a em torno do
   ponto (pix_i,pix_j) com semi-variações vertical dv e horizontal dh. */
int median(int a[][MAX_DIM], int pix_i, int pix_j, int dv, int dh)
{
    int v[MAX_DIM*MAX_DIM]; /* vizinhanca achatada */
    int w, h;       /* largura e altura da vizinhanca */
    int i, j;
    w = dh*2+1;
    h = dv*2+1;
    /* Achatar vizinhaça para fazer o sort. */
    for (i = 0; i < h; i++) 
        for (j = 0; j < w; j++) 
            v[i*w+j] = a[pix_i-dv+i][pix_j-dh+j];
    /* Ordenar vizinhança. */
    insertion_sort(v, w*h);
    /* Retornar elemento central da vizinhança ordenada. */
    return v[(w*h)/2];
}

/* insertion_sort: Ordenar o vetor v com tamanho len. */
void insertion_sort(int v[], int len)
{
    int i, j, elem;
    
    for (i=1; i < len; i++) {
        elem = v[i];
        j = i;
        while ((j > 0) && (v[j-1] > elem)) {
            v[j] = v[j-1];
            j = j - 1;
        }
        v[j] = elem;
    }
}

/* copy: Copiar m linhas e n colunas da matriz a para b. */
void copy(int m, int n, int a[][MAX_DIM], int b[][MAX_DIM])
{       
    int i, j;

    for (i = 0; i < m; i++) {
        for (j = 0; j < n; j++) {
            b[i][j] = a[i][j];
        }
    }
}

/* run_viewer_and_get_option: Executar programa externo de visualização 
   de imagens, exibir uma mensagem, obter o valor de uma opção numérica 
   e fechar o programa de vizualização. */
int run_viewer_and_get_option(char command[], char message[])
{
    pid_t pid;
    int retval;

    if ((pid = fork()) == 0) {
        setpgrp();
        system(command);
        exit(0);
    }
    retval = get_option(message);
    killpg(pid, SIGQUIT);
    return retval;
}

/* get_option: Exibir uma mensagem e obter o valor de uma opção numérica. */
int get_option(char message[])
{
    char line[MAX_LINE];
    int retval;

    do {
        printf(message);
        fgets(line, MAX_LINE, stdin);
    } while (sscanf(line, "%d", &retval) != 1);
    return retval;
}
